package bbs125

import (
	"bytes"
	"cmp"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"gitee.com/kaylee595/gkaylee"
	"github.com/PuerkitoBio/goquery"
	"github.com/kaylee595/tracerr"
	"io"
	"log/slog"
	"net/http"
	"net/url"
	"strings"
	"time"
)

type Client struct {
	httpClient *http.Client
	UserAgent  string
}

func NewClient(httpClient *http.Client, userAgent string) *Client {
	const defaultUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.8.8.9 Safari/537.36 Edg/126.0.0.0"
	return &Client{
		httpClient: cmp.Or(httpClient, http.DefaultClient),
		UserAgent:  cmp.Or(userAgent, defaultUserAgent),
	}
}

func (c *Client) do(req *http.Request) (*http.Response, error) {
	req.Header.Set("User-Agent", c.UserAgent)
	return tracerr.Wrap1(c.httpClient.Do(req))
}

var ErrContentTypeNotApplicationJson = errors.New("content-type not application/json")

func (c *Client) doMarshal(req *http.Request, result any) ([]byte, error) {
	resp, err := c.do(req)
	if err != nil {
		return nil, tracerr.Wrap(err)
	}
	defer resp.Body.Close()
	respPayloadByte, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, tracerr.Wrap(err)
	}
	if resp.StatusCode != http.StatusOK {
		return respPayloadByte, tracerr.Errorf("状态码异常, 预期: %d, 实际: %d, 响应内容: %s", http.StatusOK, resp.StatusCode, respPayloadByte)
	}
	err = json.Unmarshal(respPayloadByte, result)
	if err != nil {
		if !strings.Contains(resp.Header.Get("Content-Type"), "application/json") {
			return respPayloadByte, tracerr.Wrap(ErrContentTypeNotApplicationJson)
		}
		return respPayloadByte, tracerr.Wrap(err)
	}
	return respPayloadByte, nil
}

type ApiBase struct {
	Status int    `json:"status"`
	Msg    string `json:"msg"`
}

func (a *ApiBase) IsError() bool {
	return a.Status != 1
}

func (a *ApiBase) Error() string {
	return a.Msg
}

type RespCheckOfData struct {
	Days   int    `json:"days"`   // 共签到天数
	MDays  int    `json:"mdays"`  // 连续签到天数
	Reward int    `json:"reward"` // 共获得精币数
	QTime  string `json:"qtime"`  // 上次签到时间
	Url    string `json:"url"`
	Credit int    `json:"credit"` // 本次签到获取精数
}

type RespCheck struct {
	ApiBase
	Data RespCheckOfData `json:"data"`
}

func (r *RespCheck) IsErrorTodayChecked() bool {
	const todayIsCheck = "您今日已经签到，请明天再来！"
	return r.IsError() && strings.EqualFold(r.Msg, todayIsCheck)
}

var ErrNotLogin = errors.New("not login")
var ErrBadForm = errors.New("抱歉，您的请求来路不正确或表单验证串不符，无法提交")

type Emotion string

const (
	EmotionHappy       Emotion = "kx"    // 开心
	EmotionSad         Emotion = "ng"    // 难过
	EmotionDepressed   Emotion = "ym"    // 郁闷
	EmotionBored       Emotion = "wl"    // 无聊
	EmotionAngry       Emotion = "nu"    // 怒
	EmotionWipingSweat Emotion = "ch"    // 擦汗
	EmotionStruggling  Emotion = "fd"    // 奋斗
	EmotionLazy        Emotion = "yl"    // 慵懒
	EmotionBad         Emotion = "shuai" // 衰
)

func (c *Client) Check(ctx context.Context, e Emotion) (*RespCheck, error) {
	if e == "" {
		e = EmotionHappy
	}
	const addr = "https://bbs.125.la/plugin.php?id=dsu_paulsign:sign&operation=qiandao&infloat=1"
	formHash, err := c.GetFormHash(ctx)
	if err != nil {
		return nil, tracerr.Wrap(err)
	}
reCheck:
	var checkTwo bool
	reqPayloadM := url.Values{
		"formhash":  {formHash},
		"submit":    {"1"},
		"targerurl": {""},
		"todaysay":  {""},
		"qdxq":      {string(e)}, // 签到时的表情状态, kx表示开心
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, addr, strings.NewReader(reqPayloadM.Encode()))
	if err != nil {
		return nil, tracerr.Wrap(err)
	}
	const referer = "https://bbs.125.la/plugin.php?id=dsu_paulsign:sign"
	req.Header.Set("Referer", referer)
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
	respPayload := new(RespCheck)
	respPayloadByte, err := c.doMarshal(req, &respPayload)
	if err != nil {
		if errors.Is(err, ErrContentTypeNotApplicationJson) {
			if bytes.Contains(respPayloadByte, []byte("您需要先登录才能继续本操作")) {
				return nil, tracerr.Wrap(ErrNotLogin)
			}
			if bytes.Contains(respPayloadByte, []byte("抱歉，您的请求来路不正确或表单验证串不符，无法提交")) {
				if !checkTwo {
					// 解析最新的formHash
					newFormHash, err := c.getFormHash(bytes.NewReader(respPayloadByte))
					if err != nil {
						return nil, tracerr.Wrap(err)
					}
					// 检查刚刚是不是已经使用了最新的formHash
					if formHash == newFormHash {
						return nil, tracerr.Wrap(ErrBadForm)
					}
					// 保存最新的formHash
					formHash = newFormHash
					c.storeFormHashToCookieJar(formHash, req.URL)
					checkTwo = true
					goto reCheck
				}
				return nil, tracerr.Wrap(ErrBadForm)
			}
		}
		slog.Debug("签到失败", "响应内容", string(respPayloadByte))
		return nil, tracerr.Wrap(err)
	}
	if respPayload.IsError() {
		slog.Debug("签到失败", "响应内容", string(respPayloadByte))
		return nil, tracerr.Wrap(respPayload)
	}
	return respPayload, nil
}

var ErrGetFormHash = errors.New("获取formHash失败")

func (c *Client) GetFormHash(ctx context.Context) (string, error) {
	const addr = "https://bbs.125.la/"
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, addr, nil)
	if err != nil {
		return "", tracerr.Wrap(err)
	}
	formHash := c.getFormHashFromCookieJar(req.URL)
	if len(formHash) > 0 {
		return formHash, nil
	}
	const referer = "https://bbs.125.la/plugin.php?id=dsu_paulsign:sign"
	req.Header.Set("Referer", referer)
	resp, err := c.do(req)
	if err != nil {
		return "", tracerr.Wrap(err)
	}
	defer func() {
		gkaylee.IgnoreErr0(resp.Body.Close())
	}()
	if resp.StatusCode != http.StatusOK {
		return "", tracerr.Errorf("http状态码异常, 预期: %d, 实际: %d", http.StatusOK, resp.StatusCode)
	}
	formHash, err = c.getFormHash(resp.Body)
	if err != nil {
		return "", tracerr.Wrap(err)
	}
	c.storeFormHashToCookieJar(formHash, req.URL)
	return formHash, nil
}

func (c *Client) getFormHash(reader io.Reader) (string, error) {
	dom, err := goquery.NewDocumentFromReader(reader)
	if err != nil {
		return "", tracerr.Wrap(err)
	}
	quiteAddr, _ := dom.Find("#um > p:nth-child(2) > a:nth-child(24)").Attr("href")
	u, err := url.Parse(quiteAddr)
	if err != nil {
		return "", tracerr.Wrap(err)
	}
	formHash := u.Query().Get("formhash")
	if len(formHash) == 0 {
		loginButtonText := dom.Find("#toptb > div > div.y > div > a:nth-child(1)").Text()
		if strings.Contains(loginButtonText, "登录") {
			return "", tracerr.Wrap(ErrNotLogin)
		}
		return "", tracerr.Wrap(ErrGetFormHash)
	}
	return formHash, nil
}

func (c *Client) storeFormHashToCookieJar(formHash string, url *url.URL) {
	if c.httpClient.Jar != nil {
		c.httpClient.Jar.SetCookies(url, []*http.Cookie{
			{
				Name:   "form_hash",
				Value:  formHash,
				Domain: ".125.la",
			},
		})
	}
}

func (c *Client) getFormHashFromCookieJar(url *url.URL) string {
	if c.httpClient.Jar != nil {
		for _, cookie := range c.httpClient.Jar.Cookies(url) {
			if strings.EqualFold(cookie.Name, "form_hash") && cookie.Value != "" {
				return cookie.Value
			}
		}
	}
	return ""
}

type ReqLoginGeeTest struct {
	Challenge string
	Validate  string
}

type RespLoginOfData struct {
}

type RespLogin struct {
	ApiBase
	Data string
}

func (c *Client) Login(ctx context.Context, username, password string, validate ReqLoginGeeTest) (*RespLogin, error) {
	const addr = "https://bbs.125.la/login.htm?ac=login"
	reqPayloadM := url.Values{
		"username":          {username},
		"password":          {password},
		"questionid":        {""},
		"answer":            {""},
		"geetest_challenge": {validate.Challenge},
		"geetest_validate":  {validate.Validate},
		"geetest_seccode":   {fmt.Sprintf("%s|jordan", validate.Validate)},
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, addr, strings.NewReader(reqPayloadM.Encode()))
	if err != nil {
		return nil, tracerr.Wrap(err)
	}
	const referer = "https://bbs.125.la/login.htm"
	req.Header.Set("Referer", referer)
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
	respPayload := new(RespLogin)
	respPayloadByte, err := c.doMarshal(req, &respPayload)
	if err != nil {
		slog.Debug("登录失败", "响应内容", string(respPayloadByte))
		return nil, tracerr.Wrap(err)
	}
	if respPayload.IsError() {
		slog.Debug("登录失败", "响应内容", string(respPayloadByte))
		return nil, tracerr.Wrap(respPayload)
	}
	if c.httpClient.Jar != nil {
		// 重新登录的情况下, 需要将旧的form_hash删除, 保证cookiejar中没有旧form_hash, 这样程序在需要用到form_hash时, 会重新向服务器索取.
		c.httpClient.Jar.SetCookies(req.URL, []*http.Cookie{
			{
				Name:   "form_hash",
				Value:  "",
				Domain: ".125.la",
			},
		})
	}
	return respPayload, nil
}

type RespGeeTest struct {
	Success    int    `json:"success"`
	Gt         string `json:"gt"`
	Challenge  string `json:"challenge"`
	NewCaptcha int    `json:"new_captcha"`
}

func (r *RespGeeTest) Error() string {
	if r.Success == 1 {
		return "success"
	}
	return "fail"
}

func (r *RespGeeTest) IsError() bool {
	return r.Success != 1
}

func (c *Client) GeeTest(ctx context.Context) (*RespGeeTest, error) {
	var addr = fmt.Sprintf("https://bbs.125.la/other/geetest/geetest.php?ajax=1&t=%d", time.Now().UnixMilli())
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, addr, nil)
	if err != nil {
		return nil, tracerr.Wrap(err)
	}
	const referer = "https://bbs.125.la/login.htm"
	req.Header.Set("Referer", referer)
	respPayload := new(RespGeeTest)
	respPayloadBytes, err := c.doMarshal(req, respPayload)
	if err != nil {
		slog.Debug("获取极验参数失败", "响应内容", string(respPayloadBytes))
		return nil, tracerr.Wrap(err)
	}
	if respPayload.IsError() {
		slog.Debug("获取极验参数失败", "响应内容", string(respPayloadBytes))
		return nil, tracerr.Wrap(respPayload)
	}
	return respPayload, nil
}
